#include "StreamScanGroup.h"
#include "KernelHandler.h"
#include "ApiFile.h"
#include <iostream>

namespace GPUCC
{
	StreamScanGroup::StreamScanGroup(unsigned int Size, unsigned int MaxItemSize, KernelHandler& Handler, cl::Program* prog, cl::Context* context, cl::CommandQueue* queue) : BaseScanGroup(Size, MaxItemSize, Handler, prog, context, queue)
	{
		if (this->m_Size > 0)
		{
			this->m_StreamInfo.resize(this->m_Size);
			this->m_Chunks.resize(this->m_Size * 64 * 1024);
			this->m_ChunksInfo.resize(this->m_Size);

			this->m_Hashes.resize(this->m_Size);
		}
	}

	StreamScanGroup::~StreamScanGroup()
	{
	}
	
	void StreamScanGroup::AddItemInfo(unsigned int Size)
	{
		this->m_StreamInfo[this->m_Names.size() - 1].OriginalSize = Size;
		this->m_StreamInfo[this->m_Names.size() - 1].Finished = false;
	}

	void StreamScanGroup::SetScanFinished()
	{
		this->m_Finished.Set();

		this->m_Handler.OnStreamGroupFinished(this);
	}

	void StreamScanGroup::Scan()
	{
		cl_int Result;

		this->m_Finished.Reset();

		this->PrepareFirstChunks();

		this->m_DeviceData = cl::Buffer(*this->m_Context, CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR , // for some reason CL_MEM_HOST_WRITE_ONLY return CL_INVALID_VALUE error!
		this->m_Chunks.size(),
		this->m_Chunks.data(),
		&Result);

		if (Result != CL_SUCCESS)
		{
			std::cout << "error alloc data buffer" << std::endl;
		}
		else
		{
			this->m_DeviceInfo = cl::Buffer(*this->m_Context, CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR , // for some reason CL_MEM_HOST_WRITE_ONLY return CL_INVALID_VALUE error!
			this->m_Names.size() * sizeof(BUFFER_INFO),
			(unsigned char*)this->m_ChunksInfo.data(),
			&Result);

			if (Result != CL_SUCCESS)
			{
				std::cout << "error alloc info buffer" << std::endl;
			}
			else
			{
				this->m_DeviceStreamInfo = cl::Buffer(*this->m_Context, CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR , // for some reason CL_MEM_HOST_WRITE_ONLY return CL_INVALID_VALUE error!
				this->m_Names.size() * sizeof(STREAM_BUFFER_INFO),
				this->m_StreamInfo.data(),
				&Result);

				if (Result != CL_SUCCESS)
				{
					std::cout << "error alloc stream info buffer" << std::endl;
				}
				else
				{
					this->m_DeviceHash = cl::Buffer(*this->m_Context, CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR , // for some reason CL_MEM_HOST_WRITE_ONLY return CL_INVALID_VALUE error!
					this->m_Names.size() * sizeof(MD5Signature::MD5Array),
					this->m_Hashes.data(),
					&Result);

					if (Result != CL_SUCCESS)
					{
						std::cout << "error alloc hashes buffer" << std::endl;
					}
				}
			}
		}

		if (Result == CL_SUCCESS)
		{					
			/* NO NEED TO WRITE INPUT BUFFER!
			OpenCL buffers generally have a copy of their "bits" in host memory (this is how the contents of the buffers are called in the OpenCL spec). It is necessary because device memory is limited, and the bits are usually transferred to the device only when used by kernels.
			When you create a buffer with USE_HOST_PTR, you allow the OpenCL runtime to use the host_ptr location for this host memory copy. When a kernel will use the buffer, the bits will be copied to the device. After execution, you will need to make sure the bits are synchronized back to your host memory. This is done by callingclEnqueueMapBuffer, and the pointer returned by this function will be inside your host memory area.
			When you create a buffer with COPY_HOST_PTR, the runtime allocates a new host memory copy of the buffer, and copies your bits into it. Usually, nothing is sent to the device at this point.
			*/
			/*
			cl::make_kernel<cl::Buffer, cl::Buffer, cl::Buffer> GroupMD5Calc(*kernel);
			cl::Event evt = GroupMD5Calc(cl::EnqueueArgs(*queue, cl::NullRange, cl::NDRange(1), cl::NullRange), this->m_DeviceData, this->m_DeviceInfo, this->m_DeviceHash);
			//cl::EnqueueArgs(*queue, cl::Event link event here, cl::NullRange, cl::NDRange(1), cl::NullRange)
			evt.wait();
			*/

			cl::Kernel localKernel(*this->m_Program, "calc_md5_stream_start", &Result);

			if (Result != CL_SUCCESS)
			{
				std::cout << "error in kernel ctor" << std::endl;
			}
			else
			{
				localKernel.setArg(0, this->m_DeviceData);
				localKernel.setArg(1, this->m_DeviceInfo);
				localKernel.setArg(2, this->m_DeviceStreamInfo);
				localKernel.setArg(3, this->m_DeviceHash);
				Result = this->m_Queue->enqueueNDRangeKernel(localKernel, cl::NullRange, cl::NDRange(this->m_Names.size()), cl::NullRange, NULL, &this->m_KernelFinishEvent);
				this->m_Queue->flush();

				if (Result != CL_SUCCESS)
				{
					std::cout << "error in kernel queue" << std::endl;
				}
				else
				{
					Result = this->m_KernelFinishEvent.setCallback(CL_COMPLETE, this->hash_calculated, this);

					if (Result != CL_SUCCESS)
					{
						std::cout << "error in setting up kernel event callback" << std::endl;
					}
				}
			}
		}

		if (Result != CL_SUCCESS)
		{
			this->SetScanFinished();
		}
	}

	void CL_CALLBACK StreamScanGroup::hash_calculated(cl_event event, cl_int command_exec_status, void* user_data)
	{
		StreamScanGroup* Instance = (StreamScanGroup*)user_data;

		if (command_exec_status != CL_COMPLETE)
		{
			std::cout << "error in kernel execution, code -" << command_exec_status << std::endl;
			Instance->SetScanFinished();
		}
		else
		{
			cl_int Result = Instance->m_Queue->enqueueReadBuffer(Instance->m_DeviceStreamInfo, CL_FALSE, 0, Instance->m_Names.size() * sizeof(STREAM_BUFFER_INFO), Instance->m_StreamInfo.data(), NULL, &Instance->m_CopyStreamInfoFinishEvent);
			Instance->m_Queue->flush();

			if (Result != CL_SUCCESS)
			{
				std::cout << "error reading buffer" << std::endl;
			}
			else
			{
				Result = Instance->m_CopyStreamInfoFinishEvent.setCallback(CL_COMPLETE, Instance->stream_info_read, Instance);

				if (Result != CL_SUCCESS)
				{
					std::cout << "error in setting up read stream event callback" << std::endl;
				}
			}

			if (Result != CL_SUCCESS)
			{
				Instance->SetScanFinished();
			}
		}
	}

	void CL_CALLBACK StreamScanGroup::stream_info_read(cl_event event, cl_int command_exec_status, void* user_data)
	{
		StreamScanGroup* Instance = (StreamScanGroup*)user_data;

		if (command_exec_status != CL_COMPLETE)
		{
			std::cout << "error in kernel execution, code -" << command_exec_status << std::endl;
			Instance->SetScanFinished();
		}
		else
		{
			cl_int Result;

			bool Finished = true;

			for (unsigned int StreamIndex = 0; ((StreamIndex < Instance->m_Names.size()) && (Finished)); ++StreamIndex)
			{
				Finished = Instance->m_StreamInfo[StreamIndex].Finished;
			}

			if (Finished)
			{
				Result = Instance->m_Queue->enqueueReadBuffer(Instance->m_DeviceHash, CL_FALSE, 0, Instance->m_Names.size() * sizeof(MD5Signature::MD5Array), Instance->m_Hashes.data(), NULL, &Instance->m_CopyStreamInfoFinishEvent);
				Instance->m_Queue->flush();

				if (Result != CL_SUCCESS)
				{
					std::cout << "error reading buffer" << std::endl;
				}
				else
				{
					Result = Instance->m_CopyStreamInfoFinishEvent.setCallback(CL_COMPLETE, Instance->hash_read, Instance);

					if (Result != CL_SUCCESS)
					{
						std::cout << "error in setting up read event callback" << std::endl;
					}
				}				
			}
			else
			{
				unsigned int ChunksSize = Instance->PrepareNextChunks();

				Result = Instance->m_Queue->enqueueWriteBuffer(Instance->m_DeviceData, CL_FALSE, 0, ChunksSize, Instance->m_Chunks.data(), NULL, &Instance->m_UpdateDataFinishEvent);
				Instance->m_Queue->flush();

				if (Result != CL_SUCCESS)
				{
					std::cout << "error write buffer" << std::endl;
				}
				else
				{
					Result = Instance->m_UpdateDataFinishEvent.setCallback(CL_COMPLETE, Instance->data_writen, Instance);

					if (Result != CL_SUCCESS)
					{
						std::cout << "error in setting up write stream event callback" << std::endl;
					}
				}
			}

			if (Result != CL_SUCCESS)
			{
				Instance->SetScanFinished();
			}
		}
	}

	void CL_CALLBACK StreamScanGroup::data_writen(cl_event event, cl_int command_exec_status, void* user_data)
	{
		StreamScanGroup* Instance = (StreamScanGroup*)user_data;

		if (command_exec_status != CL_COMPLETE)
		{
			std::cout << "error in kernel execution, code -" << command_exec_status << std::endl;
			Instance->SetScanFinished();
		}
		else
		{
			cl_int Result = Instance->m_Queue->enqueueWriteBuffer(Instance->m_DeviceInfo, CL_FALSE, 0, Instance->m_Names.size() * sizeof(BUFFER_INFO), Instance->m_ChunksInfo.data(), NULL, &Instance->m_UpdateInfoFinishEvent);
			Instance->m_Queue->flush();

			if (Result != CL_SUCCESS)
			{
				std::cout << "error write buffer" << std::endl;
			}
			else
			{
				Result = Instance->m_UpdateInfoFinishEvent.setCallback(CL_COMPLETE, Instance->info_writen, Instance);

				if (Result != CL_SUCCESS)
				{
					std::cout << "error in setting up write stream event callback" << std::endl;
				}
			}

			if (Result != CL_SUCCESS)
			{
				Instance->SetScanFinished();
			}
		}
	}

	void CL_CALLBACK StreamScanGroup::info_writen(cl_event event, cl_int command_exec_status, void* user_data)
	{
		StreamScanGroup* Instance = (StreamScanGroup*)user_data;

		if (command_exec_status != CL_COMPLETE)
		{
			std::cout << "error in kernel execution, code -" << command_exec_status << std::endl;
			Instance->SetScanFinished();
		}
		else
		{
			cl_int Result;

			cl::Kernel localKernel(*Instance->m_Program, "calc_md5_stream_more", &Result);
			
			if (Result != CL_SUCCESS)
			{
				std::cout << "error in kernel ctor" << std::endl;
			}
			else
			{
				localKernel.setArg(0, Instance->m_DeviceData);
				localKernel.setArg(1, Instance->m_DeviceInfo);
				localKernel.setArg(2, Instance->m_DeviceStreamInfo);
				localKernel.setArg(3, Instance->m_DeviceHash);
				Result = Instance->m_Queue->enqueueNDRangeKernel(localKernel, cl::NullRange, cl::NDRange(Instance->m_Names.size()), cl::NullRange, NULL, &Instance->m_KernelFinishEvent);
				Instance->m_Queue->flush();

				if (Result != CL_SUCCESS)
				{
					std::cout << "error in kernel queue" << std::endl;
				}
				else
				{
					Result = Instance->m_KernelFinishEvent.setCallback(CL_COMPLETE, Instance->hash_calculated, Instance);

					if (Result != CL_SUCCESS)
					{
						std::cout << "error in setting up kernel event callback" << std::endl;
					}
				}
			}

			if (Result != CL_SUCCESS)
			{
				Instance->SetScanFinished();
			}
		}
	}

	void CL_CALLBACK StreamScanGroup::hash_read(cl_event event, cl_int command_exec_status, void* user_data)
	{
		StreamScanGroup* Instance = (StreamScanGroup*)user_data;
	
		if (command_exec_status != CL_COMPLETE)
		{
			std::cout << "error in return hash values, code -" << command_exec_status << std::endl;
			Instance->SetScanFinished();
		}
		else
		{
			Instance->m_Handler.OnKernelOutputRead(Instance);
		}
	}

	void StreamScanGroup::PrepareFirstChunks()
	{
		unsigned int ChunksBufferOffset = 0;

		for (unsigned int ItemIndex = 0; ItemIndex < this->m_Names.size(); ++ItemIndex)
		{
			this->m_ChunksInfo[ItemIndex].Offset = ChunksBufferOffset;
			this->m_ChunksInfo[ItemIndex].Size = this->m_Info[ItemIndex].Size;

			// todo: fix magic number!!!!!!
			unsigned int SizeToCopy = 64 * 1024;

			if (m_ChunksInfo[ItemIndex].Size < SizeToCopy)
			{
				SizeToCopy = this->m_Info[ItemIndex].Size;
			}

			memcpy(this->m_Chunks.data() + ChunksBufferOffset, this->m_Data.data() + this->m_Info[ItemIndex].Offset, SizeToCopy);

			ChunksBufferOffset += SizeToCopy;
		}
	}

	unsigned int StreamScanGroup::PrepareNextChunks()
	{
		unsigned int ChunksBufferOffset = 0;

		for (unsigned int ItemIndex = 0; ItemIndex < this->m_Names.size(); ++ItemIndex)
		{
			if (!this->m_StreamInfo[ItemIndex].Finished)
			{
				this->m_ChunksInfo[ItemIndex].Offset = ChunksBufferOffset;

				// todo: fix magic number!!!!!!
				unsigned int SizeToCopy = 64 * 1024;

				// todo: fix magic number!!!!!!
				this->m_ChunksInfo[ItemIndex].Size -= SizeToCopy;

				if (this->m_ChunksInfo[ItemIndex].Size < SizeToCopy)
				{
					SizeToCopy = this->m_ChunksInfo[ItemIndex].Size;
				}

				unsigned int ItemCurrentChunkOffset = this->m_Info[ItemIndex].Size - this->m_ChunksInfo[ItemIndex].Size;
				memcpy(this->m_Chunks.data() + ChunksBufferOffset, this->m_Data.data() + this->m_Info[ItemIndex].Offset + ItemCurrentChunkOffset, SizeToCopy);

				ChunksBufferOffset += SizeToCopy;
			}
		}

		return (ChunksBufferOffset);
	}
}